package com.github.eclipseace.mybatis.entity.mapper;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.ibatis.builder.IncompleteElementException;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.ResultMapResolver;
import org.apache.ibatis.builder.xml.XMLStatementBuilder;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.session.Configuration;

import com.github.eclipseace.mybatis.entity.builder.ElementBuilder;
import com.github.eclipseace.mybatis.entity.mapping.EntityResolver;
import com.github.eclipseace.mybatis.entity.mapping.MappedEntity;
import com.github.eclipseace.mybatis.entity.mapping.annotation.AnnotationEntityResolver;

/**
 * @author eclipseAce
 */
public class MapperEnhancer {
	private final Configuration configuration;
	private final EntityResolver entityResolver;

	private final List<ElementBuilder> elementBuilders = new ArrayList<>();
	private final List<StatementApplier> deferredApplier = new LinkedList<>();

	public MapperEnhancer(Configuration configuration, EntityResolver entityResolver) {
		this.configuration = configuration;
		this.entityResolver = entityResolver != null ? entityResolver : new AnnotationEntityResolver();
	}

	protected void addResultMap(MapperBuilderAssistant assistant, MappedEntity entity) {
		ResultMapResolver resultMapResolver = new ResultMapResolver(assistant, MappedEntity.LOCAL_RESULT_MAP_ID,
				entity.getType(), null, null, entity.getResultMappings(configuration), false);
		try {
			resultMapResolver.resolve();
		} catch (IncompleteElementException e) {
			configuration.addIncompleteResultMap(resultMapResolver);
		}
	}

	protected void addFragment(MapperBuilderAssistant assistant, XNode node) {
		String id = node.getStringAttribute("id");
		configuration.getSqlFragments().put(assistant.applyCurrentNamespace(id, false), node);
	}

	protected void addStatement(MapperBuilderAssistant assistant, XNode node) {
		XMLStatementBuilder statementBuilder = new XMLStatementBuilder(configuration, assistant, node);
		try {
			statementBuilder.parseStatementNode();
		} catch (IncompleteElementException e) {
			configuration.addIncompleteStatement(statementBuilder);
		}
	}

	protected void tryCompleteElements() {
		Collection<XMLStatementBuilder> builders = configuration.getIncompleteStatements();
		synchronized (builders) {
			Iterator<XMLStatementBuilder> builderIterator = builders.iterator();
			while (builderIterator.hasNext()) {
				try {
					builderIterator.next().parseStatementNode();
					builderIterator.remove();
				} catch (IncompleteElementException ignored) {
				}
			}
		}

		Iterator<StatementApplier> applierIterator = deferredApplier.iterator();
		while (applierIterator.hasNext()) {
			try {
				applierIterator.next().applyIfAbsent();
				applierIterator.remove();
			} catch (IncompleteElementException ignored) {
			}
		}
	}

	protected MapperBuilderAssistant getAssistant(Class<?> entityType) {
		String resource = "Entity [" + entityType.getName() + "]";
		MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, resource);
		assistant.setCurrentNamespace(entityType.getName());
		return assistant;
	}

	protected void applyStatementIfAbsent(Class<?> entityType, String statementId, Class<?> mapper, Method method) {
		String sourceId = entityType.getName() + "." + statementId;
		String targetId = mapper.getName() + "." + method.getName();
		StatementApplier applier = new StatementApplier(configuration, sourceId, targetId);
		try {
			applier.applyIfAbsent();
		} catch (IncompleteElementException e) {
			deferredApplier.add(applier);
		}
	}

	public boolean isEntityTypePresent(Class<?> entityType) {
		String resultMapId = entityType.getName() + "." + MappedEntity.LOCAL_RESULT_MAP_ID;
		return configuration.hasResultMap(resultMapId);
	}

	public void addElementBuilder(ElementBuilder elementBuilder) {
		this.elementBuilders.add(elementBuilder);
	}

	public void addElementBuilders(List<ElementBuilder> elementBuilders) {
		this.elementBuilders.addAll(elementBuilders);
	}

	public void addEntityType(Class<?> entityType) {
		MapperBuilderAssistant assistant = getAssistant(entityType);
		MappedEntity entity = entityResolver.resolve(configuration, entityType);
		addResultMap(assistant, entity);
		for (ElementBuilder elementBuilder : elementBuilders) {
			List<XNode> fragments = elementBuilder.getFragments(configuration, entity);
			if (fragments != null) {
				for (XNode fragment : fragments) {
					addFragment(assistant, fragment);
				}
			}

			List<XNode> statements = elementBuilder.getStatements(configuration, entity);
			if (statements != null) {
				for (XNode statement : statements) {
					addStatement(assistant, statement);
				}
			}
		}
		tryCompleteElements();
	}

	public void enhance(Class<?> mapper) {
		ApplyEntity aApplyEntity = mapper.getAnnotation(ApplyEntity.class);
		if (aApplyEntity == null) {
			return;
		}
		for (Method method : mapper.getMethods()) {
			ApplyStatement aApplyStatement = method.getAnnotation(ApplyStatement.class);
			if (aApplyStatement != null) {
				String statementId = aApplyStatement.value();
				Class<?> entityType = aApplyEntity.value();
				applyStatementIfAbsent(entityType, statementId, mapper, method);
			}
		}
	}

	public Configuration getConfiguration() {
		return configuration;
	}

	public EntityResolver getEntityResolver() {
		return entityResolver;
	}

	public List<ElementBuilder> getElementBuilders() {
		return Collections.unmodifiableList(elementBuilders);
	}
}
